﻿using System;
using System.Collections;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net.NetworkInformation;
using System.Net;

namespace passitumi
{
	/**
	 * IpPingManager is responsible for creating a set of pings for each IP address of
	 * a given peer.  It will determine which IP addresses of the given set are still valid
	 * and what is the fastest of the set.
	 */
	public class IpPingManager
	{
		public string serviceName;
		private ArrayList ipAddresses;
		private ArrayList pingThreads = new ArrayList(), pingTestResults = new ArrayList();

		// The event handler which is used to receive and handle events from the IpPingManager
		public delegate void IpPingTestSetEventHandler(object o, string serviceName, ArrayList args);
		public event IpPingTestSetEventHandler OnIpPingTestSetEvent;

		public IpPingManager(string serviceName, ArrayList ipAddresses)
		{
			Console.WriteLine("IpPingManager: Pinging {0} IP addresses for connectivity [{1}]", ipAddresses.Count, System.Threading.Thread.CurrentThread.ManagedThreadId);

			this.serviceName = serviceName;
			this.ipAddresses = ipAddresses;

			// Create an IpPingTest object for each IP address we are testing.
			for (int i = 0; i < this.ipAddresses.Count; i++) {
				IpPingTest pingTest = new IpPingTest(this.ipAddresses[i].ToString());
				pingTest.OnIpPingTestEvent += new IpPingTest.IpPingTestEventHandler(pingTestEventReceived);
				
				Thread pingThread = new Thread(new ThreadStart(pingTest.run));
				this.pingThreads.Add(pingThread);
			}

			// Start each ping test
			for (int i = 0; i < this.pingThreads.Count; i++)
			{
				Thread pingThread = this.pingThreads[i] as Thread;
				pingThread.Start();
			}
		}

		/**
		 * When a ping test has completed, this method will determine if the set of pings has completed
		 * and it is time to inform a higher level service.
		 */
		private void pingTestEventReceived(object o, IpPingTestEventArgs args)
		{
			// Lock the object that we require
			lock (this.pingTestResults)
			{
				Console.WriteLine("IpPingManager: Event received from a ping test [{0}]", System.Threading.Thread.CurrentThread.ManagedThreadId);
				this.pingTestResults.Add(args);

				// If the ping test for each ip address has completed
				if (this.pingTestResults.Count == this.ipAddresses.Count)
				{
					/*
					 * Here we remove all invalid IP addresses.  The reasoning for this
					 * is that we can pass this information on so the ServiceLocator can determine if a peer
					 * has lost an IP address or if the peer has no more valid IP addresses.
					 */
					for (int i = 0; i < this.pingTestResults.Count; i++)
					{
						IpPingTestEventArgs pingTestEvent = this.pingTestResults[i] as IpPingTestEventArgs;

						if (pingTestEvent.result != IpPingTestEventArgs.SUCCESS)
							this.pingTestResults.RemoveAt(i);
					}

					// Propogate an event to notify service of results
					OnIpPingTestSetEvent(this, this.serviceName, this.pingTestResults);
				}
				// Do nothing, as we still are waiting on results.
			}
		}

		/**
		 * IpPingTest is a class that is responsible for conducting a series
		 * of ping tests on an IP address, aborting when either a connection
		 * has been established or the maximum tries have been attempted.
		 */
		private class IpPingTest
		{
			// Constants that define the parameters of the ping test
			private const short MAX_TRIES = 3, DELAY_TIME = 5000;

			// The IP address we are testing
			private string ipAddress;

			// The event handler which is used to receive and handle events from the ServiceLocator
			public delegate void IpPingTestEventHandler(object o, IpPingTestEventArgs args);
			public event IpPingTestEventHandler OnIpPingTestEvent;

			public IpPingTest(string ipAddress)
			{
				this.ipAddress = ipAddress;
			}

			public void run()
			{
				Console.WriteLine("IpPingTest: Ping test running for IP address {0} [{1}]", this.ipAddress, System.Threading.Thread.CurrentThread.ManagedThreadId);

				Ping pingSender = new Ping();
				PingOptions pingOptions = new PingOptions();
				
				pingOptions.DontFragment = true;

				// Create a buffer of 32 bytes of data to be transmitted.
				string pingData = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
				byte[] dataBuffer = Encoding.ASCII.GetBytes(pingData);
				int timeout = 120;
				
				Console.WriteLine("IpPingTest: Delaying for {0}ms before beginning [{1}]", IpPingTest.DELAY_TIME, System.Threading.Thread.CurrentThread.ManagedThreadId);
				// Delay the next ping to give the connection time to re-establish
				System.Threading.Thread.Sleep(IpPingTest.DELAY_TIME);

				// Running the ping test on an IP address
				short counter = 1;

				while (counter <= IpPingTest.MAX_TRIES)
				{
					// Still have allocated tries available, try to ping
					
					PingReply reply = pingSender.Send(this.ipAddress, timeout, dataBuffer, pingOptions);
					if (reply.Status == IPStatus.Success)
					{
						Console.WriteLine("IpPingTest: Ping attempt {0} succeeded for IP address {1} [{2}]", counter, this.ipAddress, System.Threading.Thread.CurrentThread.ManagedThreadId);
						Console.WriteLine("Address: {0}", reply.Address.ToString());
						Console.WriteLine("RoundTrip time: {0}", reply.RoundtripTime);
						Console.WriteLine("Time to live: {0}", reply.Options.Ttl);
						Console.WriteLine("Don't fragment: {0}", reply.Options.DontFragment);
						Console.WriteLine("Buffer size: {0}", reply.Buffer.Length);

						// Create a IpPingTestEventArgs object to store the success results of this event
						OnIpPingTestEvent(this, new IpPingTestEventArgs(this.ipAddress, IpPingTestEventArgs.SUCCESS, reply.RoundtripTime));
						Console.WriteLine("IpPingTest: Thread exited [{0}]", System.Threading.Thread.CurrentThread.ManagedThreadId);
						return;
					}

					Console.WriteLine("IpPingTest: Ping attempt {0} failed for IP address {1} [{2}]", counter, this.ipAddress, System.Threading.Thread.CurrentThread.ManagedThreadId);

					// Increment the counter
					counter++;

					if (counter <= IpPingTest.MAX_TRIES)
					{
						Console.WriteLine("IpPingTest: Delaying for {0}ms [{1}]", IpPingTest.DELAY_TIME, System.Threading.Thread.CurrentThread.ManagedThreadId);
						// Delay the next ping to give the connection time to re-establish
						System.Threading.Thread.Sleep(IpPingTest.DELAY_TIME);
					}

					
				}
				// Ping test failed 3 tries, indicate to the IpPingManager of a failure
				// Create a IpPingTestEventArgs object to store the success results of this event
				OnIpPingTestEvent(this, new IpPingTestEventArgs(this.ipAddress, IpPingTestEventArgs.TIMEOUT));
				Console.WriteLine("IpPingTest: Thread exited [{0}]", System.Threading.Thread.CurrentThread.ManagedThreadId);
			}
		}
	}
}
